﻿using DXBase;
using DXBase.Structs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DXBase.D3D
{
	public class Camera : ModelBase
	{
		public Camera()
		{
			SetView(new Vector3F(0, 1.0f, 2.0f), new Vector3F(0, -1, -2), new Vector3F(0, 1, 0));
			SetProjection(70.0f, 1.0f, 0.01f, 100.0f);
		}

		public Camera Clone()
		{
			var c = new Camera();
			c.SetView(EyeLocation, LookVector, UpVector);
			c.SetProjection(FieldOfView, AspectRatio, NearPlane, FarPlane);
			return c;
		}

		#region EyeLocation

		public Vector3F EyeLocation
		{
			get { return mEyeLocation; }
			set
			{
				if (Equals(value, mEyeLocation))
					return;
				mEyeLocation = value;
				View = Transform3D.GetView(EyeLocation, LookVector, UpVector);
				OnPropertyChanged();
			}
		}
		private Vector3F mEyeLocation;

		#endregion

		#region LookVector

		public Vector3F LookVector
		{
			get { return mLookVector; }
			set
			{
				if (Equals(value, mLookVector))
					return;
				mLookVector = value;
				View = Transform3D.GetView(EyeLocation, LookVector, UpVector);
				OnPropertyChanged();
			}
		}
		private Vector3F mLookVector;

		#endregion

		#region UpVector

		public Vector3F UpVector
		{
			get { return mUpVector; }
			set
			{
				if (Equals(value, mUpVector))
					return;
				mUpVector = value;
				View = Transform3D.GetView(EyeLocation, LookVector, UpVector);
				OnPropertyChanged();
			}
		}
		private Vector3F mUpVector;

		#endregion

		#region View, SetView(), Rotate()

		public Matrix4x4F View
		{
			get { return mView; }
			private set
			{
				if (Equals(value, mView))
					return;
				mView = value;
				OnPropertyChanged();
			}
		}
		private Matrix4x4F mView;

		public void SetView(Vector3F pos, Vector3F vLook, Vector3F vUp)
		{
			mEyeLocation = pos;
			mLookVector = vLook;
			mUpVector = vUp;
			View = Transform3D.GetView(EyeLocation, LookVector, UpVector);
			OnPropertyChanged("EyeLocation");
			OnPropertyChanged("LookVector");
			OnPropertyChanged("UpVector");
		}

		public void Rotate(QuaternionF q)
		{
			SetView(EyeLocation, q * LookVector, q * UpVector);
		}

		#endregion

		#region NearPlane

		public float NearPlane
		{
			get { return mNearPlane; }
			set
			{
				if (value == mNearPlane)
					return;
				mNearPlane = value;
				Projection = Transform3D.GetProjection(FieldOfView, AspectRatio, NearPlane, FarPlane);
				OnPropertyChanged();
			}
		}
		private float mNearPlane;

		#endregion

		#region FarPlane

		public float FarPlane
		{
			get { return mFarPlane; }
			set
			{
				if (value == mFarPlane)
					return;
				mFarPlane = value;
				Projection = Transform3D.GetProjection(FieldOfView, AspectRatio, NearPlane, FarPlane);
				OnPropertyChanged();
			}
		}
		private float mFarPlane;

		#endregion

		#region FieldOfView

		public float FieldOfView
		{
			get { return mFieldOfView; }
			set
			{
				if (value == mFieldOfView)
					return;
				mFieldOfView = value;
				Projection = Transform3D.GetProjection(FieldOfView, AspectRatio, NearPlane, FarPlane);
				OnPropertyChanged();
			}
		}
		private float mFieldOfView;

		#endregion

		#region AspectRatio

		public float AspectRatio
		{
			get { return mAspectRatio; }
			set
			{
				if (value == mAspectRatio)
					return;
				mAspectRatio = value;
				Projection = Transform3D.GetProjection(FieldOfView, AspectRatio, NearPlane, FarPlane);
				OnPropertyChanged();
			}
		}
		private float mAspectRatio;

		#endregion

		#region Projection, SetProjection()

		public Matrix4x4F Projection
		{
			get { return mProjection; }
			private set
			{
				if (Equals(value, mProjection))
					return;
				mProjection = value;
				Projection = Transform3D.GetProjection(FieldOfView, AspectRatio, NearPlane, FarPlane);
				OnPropertyChanged();
			}
		}
		private Matrix4x4F mProjection;

		public void SetProjection(float fieldOfView, DXContext ctxt, float nearPlane, float farPlane)
		{
			var ratio = ctxt.PixelSize.width / (float)ctxt.PixelSize.height;
			SetProjection(fieldOfView, (float)ratio, nearPlane, farPlane);
		}

		public void SetProjection(float fieldOfView, float aspectRatio, float nearPlane, float farPlane)
		{
			mFieldOfView = fieldOfView;
			mAspectRatio = aspectRatio;
			mNearPlane = nearPlane;
			mFarPlane = farPlane;
			Projection = Transform3D.GetProjection(FieldOfView, AspectRatio, NearPlane, FarPlane);
			OnPropertyChanged("FieldOfView");
			OnPropertyChanged("AspectRatio");
			OnPropertyChanged("NearPlane");
			OnPropertyChanged("FarPlane");
		}

		public void SetProjection(DXContext context)
		{
			AspectRatio = (context.PixelSize.width / (float)context.PixelSize.height);
		}

		#endregion

		#region RayAt, PointAt, Get(View)Frustum

		/// <summary>
		/// Get a ray at a given position, in (Real / Device Dependent Pixel) pixel coordinate
		/// </summary>
		public Ray RayAt(CoordinateType ct, DXContext ctxt, float x, float y)
		{
			var r = new Ray
			{
				Direction = PointAt(ct, ctxt, x, y),
				Origin = new Vector3F(),
			};
			return View.Invert() * r;
		}

		/// <summary>
		/// Get a point at a given position
		/// </summary>
		public Vector3F PointAt(CoordinateType ct, DXContext ctxt, float x, float y)
		{
			switch (ct)
			{
				case CoordinateType.Pixels:
					{
						var s = ctxt.PixelSize;
						x /= s.width;
						y /= s.height;
					}
					break;
				default:
				case CoordinateType.DIP:
					{
						var s = DPI.GetDIP(DPI.DisplayDPI, ctxt.PixelSize);
						x /= (float)s.Width;
						y /= (float)s.Height;
					}
					break;
				case CoordinateType.TargetDPI:
					{
						var s = ctxt.PixelSize;
						x = DPI.GetDDP(ctxt.Target.Dpi, x) / s.width;
						y = DPI.GetDDP(ctxt.Target.Dpi, y) / s.height;
					}
					break;
			}
			return PointAt(x, y);
		}
		/// <summary>
		/// Get the 3D point at the <c>CoordinateType.Logical</c> position
		/// </summary>
		public Vector3F PointAt(float x, float y) { return new Vector3F((2 * x - 1) / Projection[0, 0], (1 - 2 * y) / Projection[1, 1], 1); }

		public Frustum GetFrustum(CoordinateType ct, DXContext ctxt, Box2D area)
		{
			return GetFrustum(
				PointAt(ct, ctxt, area.X, area.YMax),
				PointAt(ct, ctxt, area.XMax, area.YMax),
				PointAt(ct, ctxt, area.XMax, area.Y),
				PointAt(ct, ctxt, area.X, area.Y)
			);
		}

		public Frustum GetViewFrustum()
		{
			return GetFrustum(
				PointAt(0, 1),
				PointAt(1, 1),
				PointAt(1, 0),
				PointAt(0, 0)
			);
		}
		Frustum GetFrustum(Vector3F p00, Vector3F p10, Vector3F p11, Vector3F p01)
		{
			var r = new Frustum
			{
				Planes = new Plane3D[] 
				{
					// front
					new Plane3D(NearPlane * p00, NearPlane * p01, NearPlane * p10),
					// back
					new Plane3D(FarPlane * p00, FarPlane * p10, FarPlane * p01),
					// top
					new Plane3D(NearPlane * p01, FarPlane * p01, NearPlane * p11),
					// bottom
					new Plane3D(NearPlane * p00, NearPlane * p10, FarPlane * p00),
					// left
					new Plane3D(NearPlane * p00, FarPlane * p00, NearPlane * p01),
					// right
					new Plane3D(NearPlane * p10, NearPlane * p11, FarPlane * p10),
				}
			};
			return View.Invert() * r;
		}

		#endregion
	}

}
